// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/renderer/media/media_stream_video_capturer_source.h"

#include <utility>

#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/debug/stack_trace.h"
#include "base/location.h"
#include "base/macros.h"
#include "base/strings/utf_string_conversions.h"
#include "content/common/media/media_stream_messages.h"
#include "content/public/common/media_stream_request.h"
#include "content/renderer/media/media_stream_constraints_util.h"
#include "content/renderer/media/video_capture_impl_manager.h"
#include "content/renderer/render_thread_impl.h"
#include "media/base/bind_to_current_loop.h"
#include "media/base/limits.h"
#include "media/base/video_frame.h"
#include "media/capture/video_capturer_source.h"

namespace content {

namespace {

    // Resolutions used if the source doesn't support capability enumeration.
    struct {
        int width;
        int height;
    } const kVideoResolutions[] = { { 1920, 1080 },
        { 1280, 720 },
        { 960, 720 },
        { 640, 480 },
        { 640, 360 },
        { 320, 240 },
        { 320, 180 } };

    // Frame rates for sources with no support for capability enumeration.
    const int kVideoFrameRates[] = { 30, 60 };

    // Hard upper-bound frame rate for tab/desktop capture.
    const double kMaxScreenCastFrameRate = 120.0;

    // Allows the user to Override default power line frequency.
    const char kPowerLineFrequency[] = "googPowerLineFrequency";

    // Returns true if the value for width or height is reasonable.
    bool DimensionValueIsValid(int x)
    {
        return x > 0 && x <= media::limits::kMaxDimension;
    }

    // Returns true if the value for frame rate is reasonable.
    bool FrameRateValueIsValid(double frame_rate)
    {
        return (frame_rate > (1.0 / 60.0)) && // Lower-bound: One frame per minute.
            (frame_rate <= media::limits::kMaxFramesPerSecond);
    }

    // Returns true if the aspect ratio of |a| and |b| are equivalent to two
    // significant digits.
    bool AreNearlyEquivalentInAspectRatio(const gfx::Size& a, const gfx::Size& b)
    {
        DCHECK(!a.IsEmpty());
        DCHECK(!b.IsEmpty());
        const int aspect_ratio_a = (100 * a.width()) / a.height();
        const int aspect_ratio_b = (100 * b.width()) / b.height();
        return aspect_ratio_a == aspect_ratio_b;
    }

    // Checks if |device_info|s type is a generated content, e.g. Tab or Desktop.
    bool IsContentVideoCaptureDevice(const StreamDeviceInfo& device_info)
    {
        return device_info.device.type == MEDIA_TAB_VIDEO_CAPTURE || device_info.device.type == MEDIA_DESKTOP_VIDEO_CAPTURE;
    }

    // Interprets the properties in |constraints| to override values in |params| and
    // determine the resolution change policy.
    void SetContentCaptureParamsFromConstraints(
        const blink::WebMediaConstraints& constraints,
        MediaStreamType type,
        media::VideoCaptureParams* params)
    {
        // The default resolution change policies for tab versus desktop capture are
        // the way they are for legacy reasons.
        if (type == MEDIA_TAB_VIDEO_CAPTURE) {
            params->resolution_change_policy = media::RESOLUTION_POLICY_FIXED_RESOLUTION;
        } else if (type == MEDIA_DESKTOP_VIDEO_CAPTURE) {
            params->resolution_change_policy = media::RESOLUTION_POLICY_ANY_WITHIN_LIMIT;
        } else {
            NOTREACHED();
        }

        // If the maximum frame resolution was provided in the constraints, use it if
        // either: 1) none has been set yet; or 2) the maximum specificed is smaller
        // than the current setting.
        int width = 0;
        int height = 0;
        gfx::Size desired_max_frame_size;
        if (GetConstraintMaxAsInteger(
                constraints, &blink::WebMediaTrackConstraintSet::width, &width)
            && GetConstraintMaxAsInteger(
                constraints, &blink::WebMediaTrackConstraintSet::height, &height)
            && DimensionValueIsValid(width) && DimensionValueIsValid(height)) {
            desired_max_frame_size.SetSize(width, height);
            if (params->requested_format.frame_size.IsEmpty() || desired_max_frame_size.width() < params->requested_format.frame_size.width() || desired_max_frame_size.height() < params->requested_format.frame_size.height()) {
                params->requested_format.frame_size = desired_max_frame_size;
            }
        }

        // Set the default frame resolution if none was provided.
        if (params->requested_format.frame_size.IsEmpty()) {
            params->requested_format.frame_size.SetSize(
                MediaStreamVideoSource::kDefaultWidth,
                MediaStreamVideoSource::kDefaultHeight);
        }

        // If the maximum frame rate was provided, use it if either: 1) none has been
        // set yet; or 2) the maximum specificed is smaller than the current setting.
        double frame_rate = 0.0;
        if (GetConstraintMaxAsDouble(constraints,
                &blink::WebMediaTrackConstraintSet::frameRate,
                &frame_rate)
            && FrameRateValueIsValid(frame_rate)) {
            if (params->requested_format.frame_rate <= 0.0f || frame_rate < params->requested_format.frame_rate) {
                params->requested_format.frame_rate = frame_rate;
            }
        }

        // Set the default frame rate if none was provided.
        if (params->requested_format.frame_rate <= 0.0f) {
            params->requested_format.frame_rate = MediaStreamVideoSource::kDefaultFrameRate;
        }

        // If the minimum frame resolution was provided, compare it to the maximum
        // frame resolution to determine the intended resolution change policy.
        if (!desired_max_frame_size.IsEmpty() && GetConstraintMinAsInteger(constraints, &blink::WebMediaTrackConstraintSet::width, &width) && GetConstraintMinAsInteger(constraints, &blink::WebMediaTrackConstraintSet::height, &height) && width <= desired_max_frame_size.width() && height <= desired_max_frame_size.height()) {
            if (width == desired_max_frame_size.width() && height == desired_max_frame_size.height()) {
                // Constraints explicitly require a single frame resolution.
                params->resolution_change_policy = media::RESOLUTION_POLICY_FIXED_RESOLUTION;
            } else if (DimensionValueIsValid(width) && DimensionValueIsValid(height) && AreNearlyEquivalentInAspectRatio(gfx::Size(width, height), desired_max_frame_size)) {
                // Constraints only mention a single aspect ratio.
                params->resolution_change_policy = media::RESOLUTION_POLICY_FIXED_ASPECT_RATIO;
            } else {
                // Constraints specify a minimum resolution that is smaller than the
                // maximum resolution and has a different aspect ratio (possibly even
                // 0x0). This indicates any frame resolution and aspect ratio is
                // acceptable.
                params->resolution_change_policy = media::RESOLUTION_POLICY_ANY_WITHIN_LIMIT;
            }
        }

        DVLOG(1) << __func__ << " "
                 << media::VideoCaptureFormat::ToString(params->requested_format)
                 << " with resolution change policy "
                 << params->resolution_change_policy;
    }

    // Interprets the properties in |constraints| to override values in |params| and
    // determine the power line frequency.
    void SetPowerLineFrequencyParamFromConstraints(
        const blink::WebMediaConstraints& constraints,
        media::VideoCaptureParams* params)
    {
        int freq;
        params->power_line_frequency = media::PowerLineFrequency::FREQUENCY_DEFAULT;
        if (!GetConstraintValueAsInteger(
                constraints,
                &blink::WebMediaTrackConstraintSet::googPowerLineFrequency, &freq)) {
            return;
        }
        if (freq == static_cast<int>(media::PowerLineFrequency::FREQUENCY_50HZ))
            params->power_line_frequency = media::PowerLineFrequency::FREQUENCY_50HZ;
        else if (freq == static_cast<int>(media::PowerLineFrequency::FREQUENCY_60HZ))
            params->power_line_frequency = media::PowerLineFrequency::FREQUENCY_60HZ;
    }

    // LocalVideoCapturerSource is a delegate used by MediaStreamVideoCapturerSource
    // for local video capture. It uses the Render singleton VideoCaptureImplManager
    // to start / stop and receive I420 frames from Chrome's video capture
    // implementation. This is a main Render thread only object.
    class LocalVideoCapturerSource final : public media::VideoCapturerSource {
    public:
        explicit LocalVideoCapturerSource(const StreamDeviceInfo& device_info);
        ~LocalVideoCapturerSource() override;

        // VideoCaptureDelegate Implementation.
        void GetCurrentSupportedFormats(
            int max_requested_width,
            int max_requested_height,
            double max_requested_frame_rate,
            const VideoCaptureDeviceFormatsCB& callback) override;
        void StartCapture(const media::VideoCaptureParams& params,
            const VideoCaptureDeliverFrameCB& new_frame_callback,
            const RunningCallback& running_callback) override;
        void RequestRefreshFrame() override;
        void MaybeSuspend() override;
        void Resume() override;
        void StopCapture() override;

    private:
        void OnStateUpdate(VideoCaptureState state);
        void OnDeviceFormatsInUseReceived(const media::VideoCaptureFormats& formats);
        void OnDeviceSupportedFormatsEnumerated(
            const media::VideoCaptureFormats& formats);

        // |session_id_| identifies the capture device used for this capture session.
        const media::VideoCaptureSessionId session_id_;

        VideoCaptureImplManager* const manager_;

        const base::Closure release_device_cb_;

        // Indicates if we are capturing generated content, e.g. Tab or Desktop.
        const bool is_content_capture_;

        // These two are valid between StartCapture() and StopCapture().
        // |running_call_back_| is run when capture is successfully started, and when
        // it is stopped or error happens.
        RunningCallback running_callback_;
        base::Closure stop_capture_cb_;

        // Placeholder keeping the callback between asynchronous device enumeration
        // calls.
        VideoCaptureDeviceFormatsCB formats_enumerated_callback_;

        // Bound to the main render thread.
        base::ThreadChecker thread_checker_;

        base::WeakPtrFactory<LocalVideoCapturerSource> weak_factory_;

        DISALLOW_COPY_AND_ASSIGN(LocalVideoCapturerSource);
    };

} // namespace

LocalVideoCapturerSource::LocalVideoCapturerSource(
    const StreamDeviceInfo& device_info)
    : session_id_(device_info.session_id)
    , manager_(RenderThreadImpl::current()->video_capture_impl_manager())
    , release_device_cb_(manager_->UseDevice(session_id_))
    , is_content_capture_(IsContentVideoCaptureDevice(device_info))
    , weak_factory_(this)
{
    DCHECK(RenderThreadImpl::current());
}

LocalVideoCapturerSource::~LocalVideoCapturerSource()
{
    DCHECK(thread_checker_.CalledOnValidThread());
    release_device_cb_.Run();
}

void LocalVideoCapturerSource::GetCurrentSupportedFormats(
    int max_requested_width,
    int max_requested_height,
    double max_requested_frame_rate,
    const VideoCaptureDeviceFormatsCB& callback)
{
    DVLOG(3) << "GetCurrentSupportedFormats({ max_requested_height = "
             << max_requested_height << "}) { max_requested_width = "
             << max_requested_width << "}) { max_requested_frame_rate = "
             << max_requested_frame_rate << "})";
    DCHECK(thread_checker_.CalledOnValidThread());

    if (is_content_capture_) {
        const int width = max_requested_width ? max_requested_width : MediaStreamVideoSource::kDefaultWidth;
        const int height = max_requested_height ? max_requested_height : MediaStreamVideoSource::kDefaultHeight;
        callback.Run(media::VideoCaptureFormats(
            1, media::VideoCaptureFormat(gfx::Size(width, height), static_cast<float>(std::min(kMaxScreenCastFrameRate, max_requested_frame_rate)), media::PIXEL_FORMAT_I420)));
        return;
    }

    DCHECK(formats_enumerated_callback_.is_null());
    formats_enumerated_callback_ = callback;
    manager_->GetDeviceFormatsInUse(
        session_id_, media::BindToCurrentLoop(base::Bind(&LocalVideoCapturerSource::OnDeviceFormatsInUseReceived, weak_factory_.GetWeakPtr())));
}

void LocalVideoCapturerSource::StartCapture(
    const media::VideoCaptureParams& params,
    const VideoCaptureDeliverFrameCB& new_frame_callback,
    const RunningCallback& running_callback)
{
    DCHECK(params.requested_format.IsValid());
    DCHECK(thread_checker_.CalledOnValidThread());
    running_callback_ = running_callback;

    stop_capture_cb_ = manager_->StartCapture(
        session_id_, params, media::BindToCurrentLoop(base::Bind(&LocalVideoCapturerSource::OnStateUpdate, weak_factory_.GetWeakPtr())),
        new_frame_callback);
}

void LocalVideoCapturerSource::RequestRefreshFrame()
{
    DVLOG(3) << __func__;
    DCHECK(thread_checker_.CalledOnValidThread());
    if (stop_capture_cb_.is_null())
        return; // Do not request frames if the source is stopped.
    manager_->RequestRefreshFrame(session_id_);
}

void LocalVideoCapturerSource::MaybeSuspend()
{
    DVLOG(3) << __func__;
    DCHECK(thread_checker_.CalledOnValidThread());
    manager_->Suspend(session_id_);
}

void LocalVideoCapturerSource::Resume()
{
    DVLOG(3) << __func__;
    DCHECK(thread_checker_.CalledOnValidThread());
    manager_->Resume(session_id_);
}

void LocalVideoCapturerSource::StopCapture()
{
    DVLOG(3) << __func__;
    DCHECK(thread_checker_.CalledOnValidThread());
    // Immediately make sure we don't provide more frames.
    if (!stop_capture_cb_.is_null())
        base::ResetAndReturn(&stop_capture_cb_).Run();
    running_callback_.Reset();
    // Invalidate any potential format enumerations going on.
    formats_enumerated_callback_.Reset();
}

void LocalVideoCapturerSource::OnStateUpdate(VideoCaptureState state)
{
    DVLOG(3) << __func__ << " state = " << state;
    DCHECK(thread_checker_.CalledOnValidThread());
    if (running_callback_.is_null())
        return;
    switch (state) {
    case VIDEO_CAPTURE_STATE_STARTED:
        running_callback_.Run(true);
        break;

    case VIDEO_CAPTURE_STATE_STOPPING:
    case VIDEO_CAPTURE_STATE_STOPPED:
    case VIDEO_CAPTURE_STATE_ERROR:
    case VIDEO_CAPTURE_STATE_ENDED:
        base::ResetAndReturn(&running_callback_).Run(false);
        break;

    case VIDEO_CAPTURE_STATE_PAUSED:
    case VIDEO_CAPTURE_STATE_RESUMED:
        // Not applicable to reporting on device starts or errors.
        break;
    }
}

void LocalVideoCapturerSource::OnDeviceFormatsInUseReceived(
    const media::VideoCaptureFormats& formats_in_use)
{
    DVLOG(3) << __func__ << ", #formats received: " << formats_in_use.size();
    DCHECK(thread_checker_.CalledOnValidThread());
    // StopCapture() might have destroyed |formats_enumerated_callback_| before
    // arriving here.
    if (formats_enumerated_callback_.is_null())
        return;
    if (formats_in_use.size()) {
        base::ResetAndReturn(&formats_enumerated_callback_).Run(formats_in_use);
        return;
    }

    // The device doesn't seem to have formats in use so try and retrieve the
    // whole list of supported ones.
    manager_->GetDeviceSupportedFormats(
        session_id_,
        media::BindToCurrentLoop(
            base::Bind(
                &LocalVideoCapturerSource::OnDeviceSupportedFormatsEnumerated,
                weak_factory_.GetWeakPtr())));
}

void LocalVideoCapturerSource::OnDeviceSupportedFormatsEnumerated(
    const media::VideoCaptureFormats& formats)
{
    DVLOG(3) << __func__ << ", #formats received: " << formats.size();
    DCHECK(thread_checker_.CalledOnValidThread());
    // StopCapture() might have destroyed |formats_enumerated_callback_| before
    // arriving here.
    if (formats_enumerated_callback_.is_null())
        return;
    if (formats.size()) {
        base::ResetAndReturn(&formats_enumerated_callback_).Run(formats);
        return;
    }

    // The capture device doesn't seem to support capability enumeration, compose
    // a fallback list of capabilities.
    media::VideoCaptureFormats default_formats;
    for (const auto& resolution : kVideoResolutions) {
        for (const auto frame_rate : kVideoFrameRates) {
            default_formats.push_back(media::VideoCaptureFormat(
                gfx::Size(resolution.width, resolution.height), frame_rate,
                media::PIXEL_FORMAT_I420));
        }
    }
    base::ResetAndReturn(&formats_enumerated_callback_).Run(default_formats);
}

MediaStreamVideoCapturerSource::MediaStreamVideoCapturerSource(
    const SourceStoppedCallback& stop_callback,
    std::unique_ptr<media::VideoCapturerSource> source)
    : RenderFrameObserver(nullptr)
    , source_(std::move(source))
{
    SetStopCallback(stop_callback);
}

MediaStreamVideoCapturerSource::MediaStreamVideoCapturerSource(
    const SourceStoppedCallback& stop_callback,
    const StreamDeviceInfo& device_info,
    RenderFrame* render_frame)
    : RenderFrameObserver(render_frame)
    , source_(new LocalVideoCapturerSource(device_info))
{
    SetStopCallback(stop_callback);
    SetDeviceInfo(device_info);
}

MediaStreamVideoCapturerSource::~MediaStreamVideoCapturerSource()
{
}

void MediaStreamVideoCapturerSource::RequestRefreshFrame()
{
    source_->RequestRefreshFrame();
}

void MediaStreamVideoCapturerSource::OnHasConsumers(bool has_consumers)
{
    if (has_consumers)
        source_->Resume();
    else
        source_->MaybeSuspend();
}

void MediaStreamVideoCapturerSource::OnCapturingLinkSecured(bool is_secure)
{
    Send(new MediaStreamHostMsg_SetCapturingLinkSecured(
        device_info().session_id, device_info().device.type, is_secure));
}

void MediaStreamVideoCapturerSource::GetCurrentSupportedFormats(
    int max_requested_width,
    int max_requested_height,
    double max_requested_frame_rate,
    const VideoCaptureDeviceFormatsCB& callback)
{
    source_->GetCurrentSupportedFormats(
        max_requested_width,
        max_requested_height,
        max_requested_frame_rate,
        callback);
}

void MediaStreamVideoCapturerSource::StartSourceImpl(
    const media::VideoCaptureFormat& format,
    const blink::WebMediaConstraints& constraints,
    const VideoCaptureDeliverFrameCB& frame_callback)
{
    media::VideoCaptureParams new_params;
    new_params.requested_format = format;
    if (IsContentVideoCaptureDevice(device_info())) {
        SetContentCaptureParamsFromConstraints(
            constraints, device_info().device.type, &new_params);
    } else if (device_info().device.type == MEDIA_DEVICE_VIDEO_CAPTURE) {
        SetPowerLineFrequencyParamFromConstraints(constraints, &new_params);
    }

    is_capture_starting_ = true;
    source_->StartCapture(
        new_params, frame_callback,
        base::Bind(&MediaStreamVideoCapturerSource::OnRunStateChanged,
            base::Unretained(this)));
}

void MediaStreamVideoCapturerSource::StopSourceImpl()
{
    source_->StopCapture();
}

void MediaStreamVideoCapturerSource::OnRunStateChanged(bool is_running)
{
    if (is_capture_starting_) {
        OnStartDone(is_running ? MEDIA_DEVICE_OK
                               : MEDIA_DEVICE_TRACK_START_FAILURE);
        is_capture_starting_ = false;
    } else if (!is_running) {
        StopSource();
    }
}

const char*
MediaStreamVideoCapturerSource::GetPowerLineFrequencyForTesting() const
{
    return kPowerLineFrequency;
}

} // namespace content
